Skip to content

fix(knife): GPG-verify Node.js SHASUMS256.txt.asc before committing checksums - #2147

Open
rishuranjanofficial wants to merge 3 commits into
GoogleContainerTools:mainfrom
rishuranjanofficial:fix/gpg-verify-node-shasums
Open

fix(knife): GPG-verify Node.js SHASUMS256.txt.asc before committing checksums#2147
rishuranjanofficial wants to merge 3 commits into
GoogleContainerTools:mainfrom
rishuranjanofficial:fix/gpg-verify-node-shasums

Conversation

@rishuranjanofficial

Copy link
Copy Markdown

Problem

knife.d/update_node_archives.js computes SHA-256 checksums by downloading
each Node.js tarball directly and hashing it locally. The resulting hashes are
committed to private/extensions/node.bzl and later used by Bazel to verify
downloads.

If the nodejs.org CDN, a reverse proxy, or DNS resolution is compromised at the
time the nightly update-node-archives workflow runs, an attacker can serve a
malicious tarball. The script will compute and commit the correct SHA-256 of the
malicious file — Bazel will then accept it on every subsequent build, silently
backdooring all gcr.io/distroless/nodejs* images.

Solution

Node.js publishes a GPG-signed SHASUMS256.txt.asc alongside every release,
signed by the Node.js release team's keys (documented at
https://github.com/nodejs/node#release-keys). Verifying this signature before
trusting any checksum establishes a chain of custody back to the Node.js release
team and eliminates the attack vector above.

This PR:

  1. Imports the 10 current Node.js release GPG keys into an isolated temporary
    keyring at startup (with fallback to a secondary keyserver).
  2. Downloads SHASUMS256.txt and SHASUMS256.txt.asc for each Node.js version.
  3. Runs gpg --verify — the script hard-fails if the signature does not
    validate, preventing any bad hashes from being committed.
  4. Parses the verified SHASUMS256.txt to extract per-architecture SHA-256
    values, replacing the previous approach of hashing downloaded tarballs.
  5. Cleans up all temporary files in a finally block.

The nightly workflow already runs on ubuntu-latest, which has gpg
pre-installed. No runner or workflow changes are required.

Security impact

Scenario Before this fix After this fix
CDN/BGP/DNS MITM during nightly cron Malicious SHA-256 committed GPG verification fails; workflow aborts
nodejs.org serving wrong tarball Bad hash committed GPG verification fails; workflow aborts
SHASUMS256.txt tampered without valid key Not checked GPG verification fails; workflow aborts

Testing

Verified locally that:

  • GPG signature validation passes for Node.js 22 current release SHASUMS.
  • The SHA-256 values parsed from the verified SHASUMS file match those produced
    by the previous tarball-hash approach.
  • A tampered SHASUMS file causes the script to exit non-zero, preventing PR
    creation.

…checksums

Signed-off-by: rishuranjan <rishuranjan6@gmail.com>
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@rishuranjanofficial

Copy link
Copy Markdown
Author

@loosebazooka

I've submitted this pull request and would appreciate a review when you have some bandwidth. Please let me know if any changes or additional context are needed.

Thank you for your time and consideration.

@rishuranjanofficial

rishuranjanofficial commented Aug 10, 2026

Copy link
Copy Markdown
Author

Hi @loosebazooka @nlopezgi @bobcallaway

Gentle ping on #2147. The PR adds GPG verification of SHASUMS256.txt.asc before committing Node.js checksums, closing a supply chain attack vector in the nightly update-node-archives workflow. Happy to address any feedback. Thanks!

@lathama

lathama commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

I am not a node person but did a quick check. I feel the execSync to connect to keyserver.ubuntu.com will fail on some build environments that limit outbound network connections. I think you are trying to fix a real issue but I doubt the shell execution is the best option.

@rishuranjanofficial

Copy link
Copy Markdown
Author

Hi @lathama

Thanks for the review!

You're right - fetching from a keyserver at runtime breaks in restricted network environments, and using execSync for shell commands is fragile.

Better approach: bundle the Node.js release team's public keys directly in the repo as a committed file (sourced from https://github.com/nodejs/node#release-keys) and replace the gpg shell call with the openpgp npm package for pure Node.js verification - no system dependencies, no network calls at build time.

These are public keys, not secrets - they're already published openly by the Node.js team and are safe to commit. Think of them like a list of trusted signatures you keep on file to check documents against.

The only maintenance consideration is that Node.js occasionally adds new release signers. When that happens the keys file needs a one-line update via PR, which is better than fetching keys silently at runtime - changes go through review and are tracked in git history.

Does this direction sound good to you, or do you have a different approach in mind?

@lathama

lathama commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

@rishuranjanofficial I don't have any better ideas, just calling out what I saw. Not sure if there is any solution to this at this time.

@loosebazooka

Copy link
Copy Markdown
Member

Please don't ping random users like you've done here. The team is aware of this PR and have added it to our backlog. There is no timeline when someone will be available to review this.

@rishuranjanofficial

Copy link
Copy Markdown
Author

Apologies - I didn't mean to bother anyone, and I'll stop the pings. Thanks for the update; glad it's on the backlog. Happy to address any feedback whenever the team has bandwidth, no rush on my end.

@loosebazooka

Copy link
Copy Markdown
Member

alright thanks for your patience with this:

  1. Lets fetch and commit the nodejs release keys. Only keep active keys (maybe https://github.com/nodejs/release-keys/tree/main/gpg-only-active-keys/pubring.kbx converted to a single nodejs_keys.asc using gpg tooling). Only checking in that ascii formatted key list file.
  2. add a script to keep the keys in sync, update-node-keys or something that runs on a daily cron.

@rishuranjanofficial

Copy link
Copy Markdown
Author

Done - implemented as you outlined:

Committed the active Node.js release keys as a single knife.d/nodejs_keys.asc (exported from nodejs/release-keys/gpg-only-active-keys via gpg).
Verification now imports that in-repo file offline (no keyserver), replacing the gpg --recv-keys calls.
Added knife.d/update_node_keys.sh plus a daily update-node-keys cron that opens a PR when the keys change (mirrors the existing update-node-archives bot-PR pattern).

One expected, by-design behavior I'll call out so it's not a surprise (not a concern): verification is fail-closed against the committed keys. If Node.js introduces a brand-new release signer, a release signed only by that new key won't verify until the daily cron refreshes nodejs_keys.asc (or the script is run manually) - a self-healing ~1-day window. It only touches the internal update-node-archives maintenance job, not contributors, PRs, or builds, and it's strictly more secure than the previous static key list. PTAL.

@loosebazooka

Copy link
Copy Markdown
Member

One expected, by-design behavior I'll call out so it's not a surprise (not a concern): verification is fail-closed against the committed keys. If Node.js introduces a brand-new release signer, a release signed only by that new key won't verify until the daily cron refreshes nodejs_keys.asc (or the script is run manually) - a self-healing ~1-day window. It only touches the internal update-node-archives maintenance job, not contributors, PRs, or builds, and it's strictly more secure than the previous static key list. PTAL.

I think this is fine and should allow for manual key update by maintainers. I expect this occur somewhat infrequently.

@rishuranjanofficial

Copy link
Copy Markdown
Author

Thanks! That's everything implemented on my end:

active Node.js release keys committed as knife.d/nodejs_keys.asc
offline verification against that file (no keyserver)
update_node_keys.sh + daily update-node-keys cron, with on-demand manual refresh (run the script or trigger the workflow)

I believe this is ready for review - happy to make any further changes if needed.

@loosebazooka loosebazooka left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few minor things, but this seems almost done

Comment thread knife.d/update_node_archives.js Outdated
const base = `https://nodejs.org/dist/v${nodeVersion}`;
const [shasumsText, shasumsAsc] = await Promise.all([
fetchText(`${base}/SHASUMS256.txt`),
fetchText(`${base}/SHASUMS256.txt.asc`),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks like the .txt.asc file is inline signature, so either we use

  • gpg --verify SHASUM256.txt.sig SHASUM256.txt to validate the .txt using the .sig.asc file
    or
  • gpg --decrypt SHASUM256.txt.asc to unwrap the .txt.asc and use it

persist-credentials: false

- name: Update node release keys
run: ./knife.d/update_node_keys.sh

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know our knife script structure is a bit silly. But update_node_keys.sh should be exposed directly in the knife script to be called like ./knife update-node-keys

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch - switched to gpg --output - --decrypt SHASUMS256.txt.asc, which verifies the inline signature and returns the authenticated checksums, and dropped the separate plaintext SHASUMS256.txt fetch so nothing unsigned is ever parsed. Validated against a real release (v22.11.0): good signature from a Node.js release signer + correct checksums, and confirmed it fails closed (non-zero exit) when the signer isn't in the committed keys. Pushed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants